查看原文
其他

MoonBit支持实时可视化编程,唤起你的童年记忆「俄罗斯方块」

MoonBit运营组 MoonBit 2024-04-02


在数字化世界中,编程一直扮演着创新和想象力的重要角色。而当我们回首童年,有一款游戏可能在编程实践中发挥出意想不到的影响——那就是经典的“俄罗斯方块”。这款由俄罗斯人阿列克谢·帕基特诺夫(Алексей Пажитнов 英文:Alexey Pazhitnov)发明的小游戏,在1980年末期至1990年代初期风靡全世界。

借助 MoonBit,我们可以将这些宝贵的童年回忆融入到编程项目中。我们有机会创建个性化的俄罗斯方块游戏,并且学习如何设计和实现游戏逻辑、图形界面以及用户交互。更为重要的是,由于 MoonBit 支持实时可视化开发,你可以在开发过程中即时看到你的代码如何影响游戏的表现。这里通过视频展示,你可以实时更改代码来调整俄罗斯游戏的速度与背景颜色。


现在,让我们把焦点转向编程实践中的童年记忆—俄罗斯方块。最近,一位MoonBit用户Luoxuwei在GitHub上分享了自己用MoonBit实现了俄罗斯方块的代码。今天我们就以他分享的代码为例,具体分享一下如何用MoonBit来编写俄罗斯方块。



01什么是俄罗斯方块


俄罗斯方块原名是俄语Тетрис(英语是Tetris),这个名字来源于希腊语tetra,意思是“四”,而游戏的作者最喜欢网球(tennis)。于是,他把两个词tetra和tennis合而为一,命名为Tetris,这也就是俄罗斯方块名字的由来。

俄罗斯方块的游戏规则很简单:由小方块组成的不同形状的板块陆续从屏幕上方落下来,玩家通过调整板块的位置和方向,使它们在屏幕底部拼出完整的一条或几条。这些完整的横条会随即消失,给新落下来的板块腾出空间,与此同时,玩家得到分数奖励。没有被消除掉的方块不断堆积起来,一旦堆到屏幕顶端,玩家便告输,游戏结束。


02用 MoonBit 编写俄罗斯方块


接下来,我们将分享如何用 MoonBit 来编写俄罗斯方块?


储存整个游戏的状态


首先,我们需要使用 struct Tetris 来储存整个游戏的状态:

struct Tetris {
  mut dead:Bool
  mut grid:List[Array[Int]]
  mut piece_pool:List[PIECE]
  mut current:PIECE
  mut piece_x:Int
  mut piece_y:Int
  mut piece_shap:Array[Array[Int]]
  mut score:Int
  mut row_completed:Int
}


grid 用来保存一个画面里面每个块的颜色,比如:

0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
1 1 1 1 0 0 0 0 0 0 0


可以用来表示下图:



生成俄罗斯方块


第一步:使用 generate_piece 生成俄罗斯方块

pub func generate_piece(self:Tetris) -> Bool {
  self.current = self.get_next_piece(true)
  self.piece_shap = self.current.piece_shap()
  self.piece_x = grid_col_count/2 - self.piece_shap[0].length()/2
  self.piece_y = 0
  return check_collision(self.grid, self.piece_shap, (self.piece_x, self.piece_y))
}


先通过 get_next_piece() 来获取下一个方块,就是从 piece_pool 中取出下一个 piece,这里获取的只是一个枚举类型。

pub func get_next_piece(self:Tetris, pop:Bool) -> PIECE {
  if self.piece_pool.length() == 0 {
    self.generate_piece_pool()
  }

  let Cons(cur, n) = self.piece_pool
  if pop {
    self.piece_pool = n 
  }
  cur
}


第二步:通过 piece_shape 来获取具体的形状。俄罗斯方块每种类型的方块表示是用一个二维数组来表示,数组里的值是颜色的索引:

pub func piece_shape(self:PIECE) -> Array[Array[Int]] {
  match self {
    I => [[1, 1, 1, 1]]
    L => [[0, 0, 2],
          [2, 2, 2]]
    J => [[3, 0, 0],
          [3, 3, 3]]
    S => [[0, 4, 4],
          [4, 4, 0]]
    Z => [[5, 5, 0],
          [0, 5, 5]]
    T => [[6, 6, 6],
          [0, 6, 0]]
    O => [[7, 7],
          [7, 7]]
  }


比如 L 表示的就是 L 形的图形, 如下图:



第三步:计算 piece 的 x 坐标和 y 坐标


第四步:调用 check_collision 检查是否有冲突



控制俄罗斯方块


我们通过 step 这个函数来移动和旋转方块,根据 action 的值来进行不同的操作:

pub func step(tetris:Tetris, action:Int) {
  if tetris.dead {
    return
  }
    
  match action {
    //move left
    1 => tetris.move_piece(-1)
    //move right
    2 => tetris.move_piece(1)
    //rotate
    3 => tetris.rotate_piece()
    //instant
    4 => tetris.drop_piece(true)
    _ => ()
  }
  tetris.drop_piece(false)
}

移动俄罗斯方块:

pub func move_piece(self:Tetris, delta:Int) {
  var new_x = self.pice_x + delta
  new_x = max(0, min(new_x, (grid_col_count - self.pice_shap[0].length())))
  if check_collision(self.grid, self.pice_shap, (new_x, self.pice_y)) {
    return
  }
  self.pice_x = new_x
}


旋转俄罗斯方块:

pub func rotate_piece(self:Tetris) {
  let r = self.pice_shape.length()
  let c = self.pice_shape[0].length()
  let new_shape = Array::make(c, Array::make(r, 0))
  var i = 0
  while i<c {
    new_shape[i] = Array::make(r, 0)
    i = i+1
  }
  
  var i_c = 0
  while i_c < c {
    var i_r = 0
    while i_r < r {
      new_shape[i_c][i_r] = self.pice_shape[r-i_r-1][i_c]
      i_r = i_r + 1
    }
    i_c = i_c + 1
  }
  var new_x = self.pice_x
  if (new_x + new_shape[0].length()) > grid_col_count {
    new_x = grid_col_count - new_shap[0].length()
  }

  if check_collision(self.grid, new_shape, (new_x, self.pice_y)) {
    return
  }
  self.piece_x = new_x
  self.piece_shape = new_shape
}


掉落俄罗斯方块:

pub func drop_piece(self:Tetris, instant:Bool) {
  if instant {
    let y = get_effective_height(self.grid, self.pice_shape, (self.piece_x, self.piece_y))
    self.piece_y = y + 1
  } else {
    self.piece_y = self.piece_y + 1
  }

  if instant == false && check_collision(self.grid, self.pice_shape, (self.piece_x, self.piece_y)) == false {
    return
  } 

  self.on_piece_collision()
}

这里的instant 参数用来判断是否是快速掉落方块;使用 on_piece_collison() 查找完整的行。然后消除他们。



消除方块

当一行满的时候方块需要被消除,我们通过 on_piece_collision 来完成消除。


先将这个方块添加进去:

pub func on_piece_collision(self:Tetris) {
  // ...

  //Add the current shap to grid
  fn go1(l:List[Array[Int]], r:Int) {
    match l {
      Cons(v, n) => {
        if r < y {
          return go1(n, r + 1)
        }

        if r >= (y + len_r) {
          return
        }
        var c = 0
        while c < len_c {
          if self.pice_shap[r - y][c] == 0 {
            c = c + 1
            continue
          }
          v[c + self.piece_x] = self.piece_shape[r - y][c]
          c = c + 1
        }
        return go1(n, r + 1)
      }
      Nil => ()
    }
  }
  go1(self.grid, 0)
}


消除已经填满的行:

pub func on_piece_collision(self : Tetris) {
  //...

  //Delete the complete row
  self.row_completed = 0
  fn go2(l:List[Array[Int]]) -> List[Array[Int]] {
    match l {
      Nil => Nil
      Cons(v, n) => {
        if contain(v, 0) {
          return Cons(v, go2(n))
        } else {
          self.row_completed = self.row_completed + 1
          return go2(n)
        }
      }
    }
  }
  var new_grid:List[Array[Int]] = Nil
  new_grid = go2(self.grid)
}



使用 Moonbit External Ref 画图


根据 Tetris 中保存的信息调用 Canvas 去画图:

pub func draw(canvas : Canvas_ctx, tetris : Tetris) {
    var c = 0

    //draw backgroud
    while c < grid_col_count {
      let color = if (c%2) == 0 {0} else {1}
      canvas.set_fill_style(color)
      canvas.fill_rect(c, 0, 1, grid_row_count)
      c = c + 1
    }

    draw_piece(canvas, tetris.grid, (0, 0))
    draw_piece(canvas, tetris.piece_shape.stream(), (tetris.piece_x, tetris.piece_y))

    if tetris.dead {
      canvas.draw_game_over()
    }
}


func draw_piece(canvas:Canvas_ctx, matrix:List[Array[Int]], offset:(Int, Int)) {

    fn go(l:List[Array[Int]], r:Int, canvas:Canvas_ctx) {
      match l {
        Cons(v, n) => {
          var c = 0
          while c < v.length() {
            if v[c] == 0 {
              c = c+1
              continue
            }
            canvas.set_fill_style(v[c]+1)
            canvas.fill_rect(offset.0 + c, offset.1 + r, 1, 1)
            canvas.set_stroke_color(0)
            canvas.set_line_width(0.1)
            canvas.stroke_rect(offset.0 + c, offset.1 + r, 1, 1)
            c = c + 1
          }
          go(n, r+1, canvas)
        }
        Nil => ()
      }
    }
    go(matrix, 0, canvas)
}


JavaScript 监听和渲染


添加对键盘事件的监听:

 window.addEventListener("keydown", (e) => {
  if (!requestAnimationFrameId) return
  switch (e.key) {
    case "ArrowLeft": {
        tetris_step(tetris, 1)
        break
    }
    case "ArrowRight": {
        tetris_step(tetris, 2)
        break
    }
    case "ArrowDown": {
        tetris_step(tetris, 4)
        break
    }
    case "ArrowUp": {
        tetris_step(tetris, 3)
        break
    }
  }
}) 

更新画面,这里会调用使用 MoonBit 写的 draw (tetris_draw)函数:
function update(time = 0) {
  const deltaTime = time - lastTime
  dropCounter += deltaTime
  if (dropCounter > dropInterval) {
    tetris_step(tetris, 0);
    scoreDom.innerHTML = "score: " + tetris_score(tetris)
    dropCounter = 0
  }
  lastTime = time
  tetris_draw(context, tetris);
  requestAnimationFrameId = requestAnimationFrame(update)
}

完整的代码:

https://github.com/moonbitlang/moonbit-docs/tree/main/examples/tetris


如果你想要看视频版的教程,可以复制下方链接,也可以在B站搜索「MoonBit月兔」,前往B站观看完整版的视频教程哦~链接:

https://www.bilibili.com/video/BV1HN411E72V/?spm_id_from=333.999.0.0&vd_source=33c2b6daf1758b4a27701c07755543cf


当然,大家如果想体验一下实时coding调试俄罗斯方块,可以直接访问我们在线IDE:https://www.moonbitlang.cn/gallery/tetris/



 MoonBit 编程实践 ⁃

欢迎来到「MoonBit编程实践」本栏目将为你提供如何使用MooBit强大功能,轻松实现各种工业应用与创意项目。我们将与你分享实用的示例代码、项目构建步骤以及技术见解,无论你是编程新手还是经验丰富的开发者,都可以轻松玩转MoonBit。


我们也期待你积极地分享你的编程实践!让我们一起开启MoonBit编程之旅🎉

官方平台账号,欢迎扫码关注


MoonBit


知乎|@张宏波 / @MoonBit

Twitter丨@Moonbitlang

Bilibili丨MoonBit月兔

MoonBit用户交流群|添加小助手rael_helper

继续滑动看下一个
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存